﻿/*
This is an Open Source calendar based on the Jquery plugin Weekcalendar by Rob Monie with some additional Javascript by Jolle Carlestam. The server side is all Lasso on Knop written by Jolle Carlestam.

The file jquery.weekcalendar.js contains all the core javascript needed by the application

*/

/*
 * jQuery.weekCalendar v1.2.2-pre
 * http://www.redredred.com.au/
 *
 * Requires:
 * - jquery.weekcalendar.css
 * - jquery 1.3.x
 * - jquery-ui 1.7.x (widget, drag, drop, resize)
 *
 * Copyright (c) 2009 Rob Monie
 * Dual licensed under the MIT and GPL licenses:
 *	http://www.opensource.org/licenses/mit-license.php
 *	http://www.gnu.org/licenses/gpl.html
 *
 *	If you're after a monthly calendar plugin, check out http://arshaw.com/fullcalendar/
 
 * Additional code by Jolle Carlestam
 */

var dates = {start: "", end: ""};

(function($) {


	$.widget("ui.weekCalendar", {

		/***********************
		* Initialise calendar *
		***********************/
		_init : function() {
			var self = this;
			self._computeOptions();
			self._setupEventDelegation();
			self._renderCalendar();
			self._loadCalEvents();
			self._resizeCalendar();
			self._scrollToHour(self.options.date.getHours());

			$(window).unbind("resize.weekcalendar");
			$(window).bind("resize.weekcalendar", function() {
				self._resizeCalendar();
			});

		},

		/********************
		* public functions *
		********************/
		/*
		* Refresh the events for the currently displayed week.
		*/
		refresh : function() {
			this._clearCalendar();
			this._loadCalEvents(this.element.data("startDate")); //reload with existing week
		},

		/*
		* Clear all events currently loaded into the calendar
		*/
		clear : function() {
			this._clearCalendar();
		},

		/*
		* Go to this week
		*/
		today : function() {
			this._clearCalendar();
			this._loadCalEvents(new Date());
		},

		/*
		* Go to the previous week relative to the currently displayed week
		*/
		prevWeek : function() {
			//minus more than 1 day to be sure we're in previous week - account for daylight savings or other anomolies
			var newDate = new Date(this.element.data("startDate").getTime() - (MILLIS_IN_WEEK / 6));
			this._clearCalendar();
			this._loadCalEvents(newDate);
		},

		/*
		* Go to the next week relative to the currently displayed week
		*/
		nextWeek : function() {
			//add 8 days to be sure of being in prev week - allows for daylight savings or other anomolies
			var newDate = new Date(this.element.data("startDate").getTime() + MILLIS_IN_WEEK + (MILLIS_IN_WEEK / 7));
			this._clearCalendar();
			this._loadCalEvents(newDate);
		},

		/*
		* Reload the calendar to whatever week the date passed in falls on.
		*/
		gotoWeek : function(date) {
			this._clearCalendar();
			this._loadCalEvents(date);
		},

		/*
		* Remove an event based on it's id
		*/
		removeEvent : function(eventId) {

			var self = this;

			self.element.find(".wc-cal-event").each(function() {
				if ($(this).data("calEvent").id === eventId) {
					$(this).remove();
					return false;
				}
			});
			
			//this could be more efficient rather than running on all days regardless...
			self.element.find(".wc-day-column-inner").each(function(){
				self._adjustOverlappingEvents($(this));
			});
		},

		/*
		* Removes any events that have been added but not yet saved (have no id).
		* This is useful to call after adding a freshly saved new event.
		*/
		removeUnsavedEvents : function() {

			var self = this;

			self.element.find(".wc-new-cal-event").each(function() {
				$(this).remove();
			});
			
			//this could be more efficient rather than running on all days regardless...
			self.element.find(".wc-day-column-inner").each(function(){
				self._adjustOverlappingEvents($(this));
			});
		},

		/*
		* update an event in the calendar. If the event exists it refreshes
		* it's rendering. If it's a new event that does not exist in the calendar
		* it will be added.
		*/
		updateEvent : function (calEvent) {
			this._updateEventInCalendar(calEvent);
		},

		/*
		* Returns an array of timeslot start and end times based on
		* the configured grid of the calendar. Returns in both date and
		* formatted time based on the 'timeFormat' config option.
		*/
		getTimeslotTimes : function(date) {
			var options = this.options;
			var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
			var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), firstHourDisplayed);

			var times = []
			var startMillis = startDate.getTime();
			for (var i = 0; i < options.timeslotsPerDay; i++) {
				var endMillis = startMillis + options.millisPerTimeslot;
				times[i] = {
					start: new Date(startMillis),
					startFormatted: this._formatDate(new Date(startMillis), options.timeFormat),
					end: new Date(endMillis),
					endFormatted: this._formatDate(new Date(endMillis), options.timeFormat)
				};
				startMillis = endMillis;
			}
			return times;
		},

		formatDate : function(date, format) {
			if (format) {
				return this._formatDate(date, format);
			} else {
				return this._formatDate(date, this.options.dateFormat);
			}
		},

		formatTime : function(date, format) {
			if (format) {
				return this._formatDate(date, format);
			} else {
				return this._formatDate(date, this.options.timeFormat);
			}
		},

		getData : function(key) {
			return this._getData(key);
		},

		/*********************
		* private functions *
		*********************/
		// compute dynamic options based on other config values
		_computeOptions : function() {

			var options = this.options;

			if (options.businessHours.limitDisplay) {
				options.timeslotsPerDay = options.timeslotsPerHour * (options.businessHours.end - options.businessHours.start);
				options.millisToDisplay = (options.businessHours.end - options.businessHours.start) * 60 * 60 * 1000;
				options.millisPerTimeslot = options.millisToDisplay / options.timeslotsPerDay;
			} else {
				options.timeslotsPerDay = options.timeslotsPerHour * 24;
				options.millisToDisplay = MILLIS_IN_DAY;
				options.millisPerTimeslot = MILLIS_IN_DAY / options.timeslotsPerDay;
			}
		},

		/*
		* Resize the calendar scrollable height based on the provided function in options.
		*/
		_resizeCalendar : function () {

			var options = this.options;
			if (options && $.isFunction(options.height)) {
				var calendarHeight = options.height(this.element);
				var headerHeight = this.element.find(".wc-header").outerHeight();
				var navHeight = this.element.find(".wc-nav").outerHeight();
				this.element.find(".wc-scrollable-grid").height(calendarHeight - navHeight - headerHeight);
			}
		},

		/*
		* configure calendar interaction events that are able to use event
		* delegation for greater efficiency
		*/
		_setupEventDelegation : function() {
			var self = this;
			var options = this.options;
			this.element.click(function(event) {
				var $target = $(event.target);
				if ($target.data("preventClick")) {
					return;
				}
				if ($target.hasClass("wc-cal-event")) {
					options.eventClick($target.data("calEvent"), $target, event);
				} else if ($target.parent().hasClass("wc-cal-event")) {
					options.eventClick($target.parent().data("calEvent"), $target.parent(), event);
				}
			}).mouseover(function(event) {
				var $target = $(event.target);

				if (self._isDraggingOrResizing($target)) {
					return;
				}

				if ($target.hasClass("wc-cal-event")) {
					options.eventMouseover($target.data("calEvent"), $target, event);
				}
			}).mouseout(function(event) {
				var $target = $(event.target);
				if (self._isDraggingOrResizing($target)) {
					return;
				}
				if ($target.hasClass("wc-cal-event")) {
					if ($target.data("sizing")) return;
					options.eventMouseout($target.data("calEvent"), $target, event);

				}
			});
		},

		/*
		* check if a ui draggable or resizable is currently being dragged or resized
		*/
		_isDraggingOrResizing : function ($target) {
			return $target.hasClass("ui-draggable-dragging") || $target.hasClass("ui-resizable-resizing");
		},

		/*
		* Render the main calendar layout
		*/
		_renderCalendar : function() {

			var $calendarContainer, calendarNavHtml, calendarHeaderHtml, calendarBodyHtml, $weekDayColumns;
			var self = this;
			var options = this.options;

			$calendarContainer = $("<div class=\"wc-container\">").appendTo(self.element);

			if (options.buttons) {
				calendarNavHtml = "<div class=\"wc-nav\">\
						 <button class=\"wc-today\">" + options.buttonText.today + "</button>\
						 <button class=\"wc-prev\">" + options.buttonText.lastWeek + "</button>\
						 <button class=\"wc-next\">" + options.buttonText.nextWeek + "</button>\
						 </div>";

				$(calendarNavHtml).appendTo($calendarContainer);

				$calendarContainer.find(".wc-nav .wc-today").click(function() {
					self.element.weekCalendar("today");
					return false;
				});

				$calendarContainer.find(".wc-nav .wc-prev").click(function() {
					self.element.weekCalendar("prevWeek");
					return false;
				});

				$calendarContainer.find(".wc-nav .wc-next").click(function() {
					self.element.weekCalendar("nextWeek");
					return false;
				});

			}

			//render calendar header
			// Week number code added by Jolle 2009-08-10
			calendarHeaderHtml = "<table class=\"wc-header\"><tbody><tr><td class=\"wc-time-column-header\">" + (this.options.useWeekNo ? "<div class=\"wc-weekNo\"></div>" : "") + "</td>";
			for (var i = 1; i <= options.daysToShow; i++) {
				calendarHeaderHtml += "<td class=\"wc-day-column-header wc-day-" + i + "\"></td>";
			}
			calendarHeaderHtml += "<td class=\"wc-scrollbar-shim\"></td></tr></tbody></table>";

			//render calendar body
			calendarBodyHtml = "<div class=\"wc-scrollable-grid\">\
					<table class=\"wc-time-slots\">\
					<tbody>\
					<tr>\
					<td class=\"wc-grid-timeslot-header\"></td>\
					<td colspan=\"" + options.daysToShow + "\">\
					<div class=\"wc-time-slot-wrapper\">\
					<div class=\"wc-time-slots\">";

			var start = options.businessHours.limitDisplay ? options.businessHours.start : 0;
			var end = options.businessHours.limitDisplay ? options.businessHours.end : 24;

			for (var i = start; i < end; i++) {
				for (var j = 0; j < options.timeslotsPerHour - 1; j++) {
					calendarBodyHtml += "<div class=\"wc-time-slot\"></div>";
				}
				calendarBodyHtml += "<div class=\"wc-time-slot wc-hour-end\"></div>";
			}

			calendarBodyHtml += "</div></div></td></tr><tr><td class=\"wc-grid-timeslot-header\">";

			for (var i = start; i < end; i++) {

				var bhClass = (options.businessHours.start <= i && options.businessHours.end > i) ? "wc-business-hours" : "";
				calendarBodyHtml += "<div class=\"wc-hour-header " + bhClass + "\">"
				if (options.use24Hour) {
					calendarBodyHtml += "<div class=\"wc-time-header-cell\">" + self._24HourForIndex(i) + "</div>";
				} else {
					calendarBodyHtml += "<div class=\"wc-time-header-cell\">" + self._hourForIndex(i) + "<span class=\"wc-am-pm\">" + self._amOrPm(i) + "</span></div>";
				}
				calendarBodyHtml += "</div>";
			}

			calendarBodyHtml += "</td>";

			for (var i = 1; i <= options.daysToShow; i++) {
				calendarBodyHtml += "<td class=\"wc-day-column day-" + i + "\"><div class=\"wc-day-column-inner\"></div></td>"
			}

			calendarBodyHtml += "</tr></tbody></table></div>";

			//append all calendar parts to container
			$(calendarHeaderHtml + calendarBodyHtml).appendTo($calendarContainer);

			$weekDayColumns = $calendarContainer.find(".wc-day-column-inner");
			$weekDayColumns.each(function(i, val) {
				$(this).height(options.timeslotHeight * options.timeslotsPerDay);
				if (!options.readonly) {
					self._addDroppableToWeekDay($(this));
					self._setupEventCreationForWeekDay($(this));
				}
			});

			$calendarContainer.find(".wc-time-slot").height(options.timeslotHeight - 1); //account for border

			$calendarContainer.find(".wc-time-header-cell").css({
				height :  (options.timeslotHeight * options.timeslotsPerHour) - 11,
				padding: 5
			});


		},

		/*
		* setup mouse events for capturing new events
		*/
		_setupEventCreationForWeekDay : function($weekDay) {
			var self = this;
			var options = this.options;
			$weekDay.mousedown(function(event) {
				var $target = $(event.target);
				if ($target.hasClass("wc-day-column-inner")) {

					var $newEvent = $("<div class=\"wc-cal-event wc-new-cal-event wc-new-cal-event-creating\"></div>");

					$newEvent.css({lineHeight: (options.timeslotHeight - 2) + "px", fontSize: (options.timeslotHeight / 2) + "px"});
					$target.append($newEvent);

					var columnOffset = $target.offset().top;
					var clickY = event.pageY - columnOffset;
					var clickYRounded = (clickY - (clickY % options.timeslotHeight)) / options.timeslotHeight;
					var topPosition = clickYRounded * options.timeslotHeight;
					$newEvent.css({top: topPosition});

					$target.bind("mousemove.newevent", function(event) {
						$newEvent.show();
						$newEvent.addClass("ui-resizable-resizing");
						var height = Math.round(event.pageY - columnOffset - topPosition);
						var remainder = height % options.timeslotHeight;
						//snap to closest timeslot
						if (remainder < (height / 2)) {
							var useHeight = height - remainder;
							$newEvent.css("height", useHeight < options.timeslotHeight ? options.timeslotHeight : useHeight);
						} else {
							$newEvent.css("height", height + (options.timeslotHeight - remainder));
						}
					}).mouseup(function() {
						$target.unbind("mousemove.newevent");
						$newEvent.addClass("ui-corner-all");
					});
				}

			}).mouseup(function(event) {
				var $target = $(event.target);

				var $weekDay = $target.closest(".wc-day-column-inner");
				var $newEvent = $weekDay.find(".wc-new-cal-event-creating");

				if ($newEvent.length) {
					//if even created from a single click only, default height
					if (!$newEvent.hasClass("ui-resizable-resizing")) {
						$newEvent.css({height: options.timeslotHeight * options.defaultEventLength}).show();
					}
					var top = parseInt($newEvent.css("top"));
					var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $newEvent, top);

					$newEvent.remove();
					var newCalEvent = {start: eventDuration.start, end: eventDuration.end, title: options.newEventText};
					var $renderedCalEvent = self._renderEvent(newCalEvent, $weekDay);

					if (!options.allowCalEventOverlap) {
						self._adjustForEventCollisions($weekDay, $renderedCalEvent, newCalEvent, newCalEvent);
						self._positionEvent($weekDay, $renderedCalEvent);
					} else {
						self._adjustOverlappingEvents($weekDay);
					}

					options.eventNew(eventDuration, $renderedCalEvent);
				}
			});
		},

		/*
		* load calendar events for the week based on the date provided
		*/
		_loadCalEvents : function(dateWithinWeek) {

			var date, weekStartDate, endDate, $weekDayColumns;
			var self = this;
			var options = this.options;
			date = dateWithinWeek || options.date;
			weekStartDate = self._dateFirstDayOfWeek(date);
			weekEndDate = self._dateLastMilliOfWeek(date);

			dates.start = self.element.weekCalendar("formatDate", weekStartDate);
			dates.end = self.element.weekCalendar("formatDate", weekEndDate);

			options.calendarBeforeLoad(self.element);

			self.element.data("startDate", weekStartDate);
			self.element.data("endDate", weekEndDate);

			$weekDayColumns = self.element.find(".wc-day-column-inner");

			self._updateDayColumnHeader($weekDayColumns);

			//load events by chosen means
			if (typeof options.data == 'string') {
				if (options.loading) options.loading(true);
				var jsonOptions = {};
				jsonOptions[options.startParam || 'start'] = Math.round(weekStartDate.getTime() / 1000);
				jsonOptions[options.endParam || 'end'] = Math.round(weekEndDate.getTime() / 1000);
				$.getJSON(options.data, jsonOptions, function(data) {
					self._renderEvents(data, $weekDayColumns);
					if (options.loading) options.loading(false);
				});
			}
			else if ($.isFunction(options.data)) {
				options.data(weekStartDate, weekEndDate,
						function(data) {
							self._renderEvents(data, $weekDayColumns);
						});
			}
			else if (options.data) {
					self._renderEvents(options.data, $weekDayColumns);
				}

			self._disableTextSelect($weekDayColumns);


		},

		/*
		* update the display of each day column header based on the calendar week
		*/
		_updateDayColumnHeader : function ($weekDayColumns) {
			var self = this;
			var options = this.options;
			var currentDay = self._cloneDate(self.element.data("startDate"));

			self.element.find(".wc-header td.wc-day-column-header").each(function(i, val) {

				var dayName = options.useShortDayNames ? options.shortDays[currentDay.getDay()] : options.longDays[currentDay.getDay()];

				$(this).html(dayName + "<br/>" + self._formatDate(currentDay, options.dateFormat));
				if (self._isToday(currentDay)) {
					$(this).addClass("wc-today");
				} else {
					$(this).removeClass("wc-today");
				}
				if(self._isHoliday(currentDay) && options.useHolidayCSS) {
					$(this).addClass("wc-holiday");
				} else {
					$(this).removeClass("wc-holiday");
				}
				currentDay = self._addDays(currentDay, 1);

			});

			currentDay = self._dateFirstDayOfWeek(self._cloneDate(self.element.data("startDate")));

			// Added by Jolle 2009-08-10
			// Inserts a week number in the upper left corner of the calendar
			if (options.useWeekNo) {
				$(".wc-weekNo").html(currentDay.getWeek())
			}

			$weekDayColumns.each(function(i, val) {

				$(this).data("startDate", self._cloneDate(currentDay));
				$(this).data("endDate", new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
				if (self._isToday(currentDay)) {
					$(this).parent().addClass("wc-today");
				} else {
					$(this).parent().removeClass("wc-today");
				}
				if(self._isHoliday(currentDay) && options.useHolidayCSS) {
					$(this).parent().addClass("wc-holiday");
				} else {
					$(this).parent().removeClass("wc-holiday");
				}

				currentDay = self._addDays(currentDay, 1);
			});

		},

		/*
		* Render the events into the calendar
		*/
		_renderEvents : function (events, $weekDayColumns) {
			var self = this;
			var options = this.options;
			var eventsToRender;

			if ($.isArray(events)) {
				eventsToRender = self._cleanEvents(events);
			} else if (events.events) {
				eventsToRender = self._cleanEvents(events.events);
			}
			if (events.options) {

				var updateLayout = false;
				//update options
				$.each(events.options, function(key, value) {
					if (value !== options[key]) {
						options[key] = value;
						updateLayout = true;
					}
				});

				self._computeOptions();

				if (updateLayout) {
					self.element.empty();
					self._renderCalendar();
					$weekDayColumns = self.element.find(".wc-time-slots .wc-day-column-inner");
					self._updateDayColumnHeader($weekDayColumns);
					self._resizeCalendar();
				}

			}


			$.each(eventsToRender, function(i, calEvent) {

				var $weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns);

				if ($weekDay) {
					self._renderEvent(calEvent, $weekDay);
				}
			});

			$weekDayColumns.each(function() {
				self._adjustOverlappingEvents($(this));
			});

			options.calendarAfterLoad(self.element);

			if (!eventsToRender.length) {
				options.noEvents();
			}

		},

		/*
		* Render a specific event into the day provided. Assumes correct
		* day for calEvent date
		*/
		_renderEvent: function (calEvent, $weekDay) {
			var self = this;
			var options = this.options;
			if (calEvent.start.getTime() > calEvent.end.getTime()) {
				return; // can't render a negative height
			}

			var eventClass, eventHtml, $calEvent, $modifiedEvent;

			eventClass = calEvent.id ? "wc-cal-event" : "wc-cal-event wc-new-cal-event";
			eventHtml = "<div class=\"" + eventClass + " ui-corner-all\">\
					<div class=\"wc-time ui-corner-all\"></div>\
					<div class=\"wc-summary\"></div>\
					<div class=\"wc-location\"></div></div>";

			$calEvent = $(eventHtml);
			$modifiedEvent = options.eventRender(calEvent, $calEvent);
			$calEvent = $modifiedEvent ? $modifiedEvent.appendTo($weekDay) : $calEvent.appendTo($weekDay);
			$calEvent.css({lineHeight: (options.timeslotHeight - 2) + "px", fontSize: ((options.timeslotHeight / 2)) + "px"});

			self._refreshEventDetails(calEvent, $calEvent);
			self._positionEvent($weekDay, $calEvent);
			$calEvent.show();

			if (!options.readonly && options.resizable(calEvent, $calEvent)) {
				self._addResizableToCalEvent(calEvent, $calEvent, $weekDay)
			}
			if (!options.readonly && options.draggable(calEvent, $calEvent)) {
				self._addDraggableToCalEvent(calEvent, $calEvent);
			}

			options.eventAfterRender(calEvent, $calEvent);

			return $calEvent;

		},

		_adjustOverlappingEvents : function($weekDay) {
			var self = this;
			if (self.options.allowCalEventOverlap) {
				var groupsList = self._groupOverlappingEventElements($weekDay);
				$.each(groupsList, function() {
					var curGroups = this;
					$.each(curGroups, function(groupIndex) {
						var curGroup = this;

						// do we want events to be displayed as overlapping
						if (self.options.overlapEventsSeparate) {
							var newWidth = 100 / curGroups.length;
							var newLeft = groupIndex * newWidth;
						} else {
							// TODO what happens when the group has more than 10 elements
							var newWidth = 100 - ( (curGroups.length - 1) * 10 );
							var newLeft = groupIndex * 10;
						}
						$.each(curGroup, function() {
							// bring mouseovered event to the front
							if (!self.options.overlapEventsSeparate) {
								$(this).bind("mouseover.z-index", function() {
									var $elem = $(this);
									$.each(curGroup, function() {
										$(this).css({"z-index":  "1"});
									});
									$elem.css({"z-index": "3"});
								});
							}
							$(this).css({width: newWidth + "%", left:newLeft + "%", right: 0});
						});
					});
				});
			}
		},


		/*
		* Find groups of overlapping events
		*/
		_groupOverlappingEventElements : function($weekDay) {
			var $events = $weekDay.find(".wc-cal-event:visible");
			var sortedEvents = $events.sort(function(a, b) {
				return $(a).data("calEvent").start.getTime() - $(b).data("calEvent").start.getTime();
			});

			var lastEndTime = new Date(0, 0, 0);
			var groups = [];
			var curGroups = [];
			var $curEvent;
			$.each(sortedEvents, function() {
				$curEvent = $(this);
				//checks, if the current group list is not empty, if the overlapping is finished
				if (curGroups.length > 0) {
					if (lastEndTime.getTime() <= $curEvent.data("calEvent").start.getTime()) {
						//finishes the current group list by adding it to the resulting list of groups and cleans it

						groups.push(curGroups);
						curGroups = [];
					}
				}

				//finds the first group to fill with the event
				for (var groupIndex = 0; groupIndex < curGroups.length; groupIndex++) {
					if (curGroups[groupIndex].length > 0) {
						//checks if the event starts after the end of the last event of the group
						if (curGroups[groupIndex][curGroups [groupIndex].length - 1].data("calEvent").end.getTime() <= $curEvent.data("calEvent").start.getTime()) {
							curGroups[groupIndex].push($curEvent);
							if (lastEndTime.getTime() < $curEvent.data("calEvent").end.getTime()) {
								lastEndTime = $curEvent.data("calEvent").end;
							}
							return;
						}
					}
				}
				//if not found, creates a new group
				curGroups.push([$curEvent]);
				if (lastEndTime.getTime() < $curEvent.data("calEvent").end.getTime()) {
					lastEndTime = $curEvent.data("calEvent").end;
				}
			});
			//adds the last groups in result
			if (curGroups.length > 0) {
				groups.push(curGroups);
			}
			return groups;
		},


		/*
		* find the weekday in the current calendar that the calEvent falls within
		*/
		_findWeekDayForEvent : function(calEvent, $weekDayColumns) {

			var $weekDay;
			$weekDayColumns.each(function() {
				if ($(this).data("startDate").getTime() <= calEvent.start.getTime() && $(this).data("endDate").getTime() >= calEvent.end.getTime()) {
					$weekDay = $(this);
					return false;
				}
			});
			return $weekDay;
		}
		,

		/*
		* update the events rendering in the calendar. Add if does not yet exist.
		*/
		_updateEventInCalendar : function (calEvent) {
			var self = this;
			var options = this.options;
			self._cleanEvent(calEvent);

			if (calEvent.id) {
				self.element.find(".wc-cal-event").each(function() {
					if ($(this).data("calEvent").id === calEvent.id || $(this).hasClass("wc-new-cal-event")) {
						$(this).remove();
						return false;
					}
				});
			}

			var $weekDay = self._findWeekDayForEvent(calEvent, self.element.find(".wc-time-slots .wc-day-column-inner"));
			if ($weekDay) {
				var $calEvent = self._renderEvent(calEvent, $weekDay);
				self._adjustForEventCollisions($weekDay, $calEvent, calEvent, calEvent);
				self._refreshEventDetails(calEvent, $calEvent);
				self._positionEvent($weekDay, $calEvent);
				self._adjustOverlappingEvents($weekDay);
			}
		}
		,

		/*
		* Position the event element within the weekday based on it's start / end dates.
		*/
		_positionEvent : function($weekDay, $calEvent) {
			var options = this.options;
			var calEvent = $calEvent.data("calEvent");
			var pxPerMillis = $weekDay.height() / options.millisToDisplay;
			var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
			var startMillis = calEvent.start.getTime() - new Date(calEvent.start.getFullYear(), calEvent.start.getMonth(), calEvent.start.getDate(), firstHourDisplayed).getTime();
			var eventMillis = calEvent.end.getTime() - calEvent.start.getTime();
			var pxTop = pxPerMillis * startMillis;
			var pxHeight = pxPerMillis * eventMillis;
			$calEvent.css({top: pxTop, height: pxHeight});
		}
		,

		/*
		* Determine the actual start and end times of a calevent based on it's
		* relative position within the weekday column and the starting hour of the
		* displayed calendar.
		*/
		_getEventDurationFromPositionedEventElement : function($weekDay, $calEvent, top) {
			var options = this.options;
			var startOffsetMillis = options.businessHours.limitDisplay ? options.businessHours.start * 60 * 60 * 1000 : 0;
			var start = new Date($weekDay.data("startDate").getTime() + startOffsetMillis + Math.round(top / options.timeslotHeight) * options.millisPerTimeslot);
			var end = new Date(start.getTime() + ($calEvent.height() / options.timeslotHeight) * options.millisPerTimeslot);
			return {start: start, end: end};
		}
		,

		/*
		* If the calendar does not allow event overlap, adjust the start or end date if necessary to
		* avoid overlapping of events. Typically, shortens the resized / dropped event to it's max possible
		* duration based on the overlap. If no satisfactory adjustment can be made, the event is reverted to
		* it's original location.
		*/
		_adjustForEventCollisions : function($weekDay, $calEvent, newCalEvent, oldCalEvent, maintainEventDuration) {
			var options = this.options;

			if (options.allowCalEventOverlap) {
				return;
			}
			var adjustedStart, adjustedEnd;
			var self = this;

			$weekDay.find(".wc-cal-event").not($calEvent).each(function() {
				var currentCalEvent = $(this).data("calEvent");

				//has been dropped onto existing event overlapping the end time
				if (newCalEvent.start.getTime() < currentCalEvent.end.getTime()
						&& newCalEvent.end.getTime() >= currentCalEvent.end.getTime()) {

					adjustedStart = currentCalEvent.end;
				}


				//has been dropped onto existing event overlapping the start time
				if (newCalEvent.end.getTime() > currentCalEvent.start.getTime()
						&& newCalEvent.start.getTime() <= currentCalEvent.start.getTime()) {

					adjustedEnd = currentCalEvent.start;
				}
				//has been dropped inside existing event with same or larger duration
				if (! oldCalEvent.resizable || (newCalEvent.end.getTime() <= currentCalEvent.end.getTime()
						&& newCalEvent.start.getTime() >= currentCalEvent.start.getTime())) {

					adjustedStart = oldCalEvent.start;
					adjustedEnd = oldCalEvent.end;
					return false;
				}

			});


			newCalEvent.start = adjustedStart || newCalEvent.start;

			if (adjustedStart && maintainEventDuration) {
				newCalEvent.end = new Date(adjustedStart.getTime() + (oldCalEvent.end.getTime() - oldCalEvent.start.getTime()));
				self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, oldCalEvent);
			} else {
				newCalEvent.end = adjustedEnd || newCalEvent.end;
			}


			//reset if new cal event has been forced to zero size
			if (newCalEvent.start.getTime() >= newCalEvent.end.getTime()) {
				newCalEvent.start = oldCalEvent.start;
				newCalEvent.end = oldCalEvent.end;
			}

			$calEvent.data("calEvent", newCalEvent);
		}
		,

		/*
		* Add draggable capabilities to an event
		*/
		_addDraggableToCalEvent : function(calEvent, $calEvent) {
			var self = this;
			var options = this.options;
			var $weekDay = self._findWeekDayForEvent(calEvent, self.element.find(".wc-time-slots .wc-day-column-inner"));
			$calEvent.draggable({
				handle : ".wc-time",
				containment: ".wc-scrollable-grid",
				revert: 'valid',
				opacity: 0.5,
				grid : [$calEvent.outerWidth() + 1, options.timeslotHeight ],
				start : function(event, ui) {
					var $calEvent = ui.draggable;
					options.eventDrag(calEvent, $calEvent);
				}
			});

		}
		,

		/*
		* Add droppable capabilites to weekdays to allow dropping of calEvents only
		*/
		_addDroppableToWeekDay : function($weekDay) {
			var self = this;
			var options = this.options;
			$weekDay.droppable({
				accept: ".wc-cal-event",
				drop: function(event, ui) {
					var $calEvent = ui.draggable;
					var top = Math.round(parseInt(ui.position.top));
					var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $calEvent, top);
					var calEvent = $calEvent.data("calEvent");
					var newCalEvent = $.extend(true, {start: eventDuration.start, end: eventDuration.end}, calEvent);
					self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent, true);
					var $weekDayColumns = self.element.find(".wc-day-column-inner");
					var $newEvent = self._renderEvent(newCalEvent, self._findWeekDayForEvent(newCalEvent, $weekDayColumns));
					$calEvent.hide();

					//trigger drop callback
					options.eventDrop(newCalEvent, calEvent, $newEvent);
					$calEvent.data("preventClick", true);

					var $weekDayOld = self._findWeekDayForEvent($calEvent.data("calEvent"), self.element.find(".wc-time-slots .wc-day-column-inner"));

					if ($weekDayOld.data("startDate") != $weekDay.data("startDate")) {
						self._adjustOverlappingEvents($weekDayOld);
					}
					self._adjustOverlappingEvents($weekDay);

					setTimeout(function() {
						$calEvent.remove();
					}, 1000);

				}
			});
		}
		,

		/*
		* Add resizable capabilities to a calEvent
		*/
		_addResizableToCalEvent : function(calEvent, $calEvent, $weekDay) {
			var self = this;
			var options = this.options;
			$calEvent.resizable({
				grid: options.timeslotHeight,
				containment : $weekDay,
				handles: "s",
				minHeight: options.timeslotHeight,
				stop :function(event, ui) {
					var $calEvent = ui.element;
					var newEnd = new Date($calEvent.data("calEvent").start.getTime() + ($calEvent.height() / options.timeslotHeight) * options.millisPerTimeslot);
					var newCalEvent = $.extend(true, {start: calEvent.start, end: newEnd}, calEvent);
					self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent);

					self._refreshEventDetails(newCalEvent, $calEvent);
					self._positionEvent($weekDay, $calEvent);
					self._adjustOverlappingEvents($weekDay);
					//trigger resize callback
					options.eventResize(newCalEvent, calEvent, $calEvent);
					$calEvent.data("preventClick", true);
					setTimeout(function() {
						$calEvent.removeData("preventClick");
					}, 500);
				}
			});
		}
		,

		/*
		* Refresh the displayed details of a calEvent in the calendar
		*/
		_refreshEventDetails : function(calEvent, $calEvent) {
			var self = this;
			var options = this.options;
			$calEvent.find(".wc-time").text(self._formatDate(calEvent.start, options.timeFormat) + options.timeSeparator + self._formatDate(calEvent.end, options.timeFormat));
			$calEvent.find(".wc-summary").text(calEvent.summary);
			$calEvent.find(".wc-location").text(calEvent.location);
			$calEvent.data("calEvent", calEvent);
		}
		,

		/*
		* Clear all cal events from the calendar
		*/
		_clearCalendar : function() {
			this.element.find(".wc-day-column-inner div").remove();
		}
		,

		/*
		* Scroll the calendar to a specific hour
		*/
		_scrollToHour : function(hour) {
			var self = this;
			var options = this.options;
			var $scrollable = this.element.find(".wc-scrollable-grid");
			var slot = hour;
			if (self.options.businessHours.limitDisplay) {
				if (hour < self.options.businessHours.start) {
					slot = 0;
				} else if (hour > self.options.businessHours.end) {
					slot = self.options.businessHours.end - self.options.businessHours.start - 1;
				}
			}

			var $target = this.element.find(".wc-grid-timeslot-header .wc-hour-header:eq(" + slot + ")");

			$scrollable.animate({scrollTop: 0}, 0, function() {
				var targetOffset = $target.offset().top;
				var scroll = targetOffset - $scrollable.offset().top - $target.outerHeight();
				$scrollable.animate({scrollTop: scroll}, options.scrollToHourMillis);
			});
		}
		,

		/*
		* find the hour (12 hour day) for a given hour index
		*/
		_hourForIndex : function(index) {
			if (index === 0) { //midnight
				return 12;
			} else if (index < 13) { //am
				return index;
			} else { //pm
				return index - 12;
			}
		}
		,

		_24HourForIndex : function(index) {
			if (index === 0) { //midnight
				return "00:00";
			} else if (index < 10) {
				return "0" + index + ":00";
			} else {
				return index + ":00";
			}
		}
		,

		_amOrPm : function (hourOfDay) {
			return hourOfDay < 12 ? "AM" : "PM";
		}
		,

		_isToday : function(date) {
			var clonedDate = this._cloneDate(date);
			this._clearTime(clonedDate);
			var today = new Date();
			this._clearTime(today);
			return today.getTime() === clonedDate.getTime();
		}
		,

		_isHoliday : function(date,start,end) {		
			var clonedDate = this._cloneDate(date);
			this._clearTime(clonedDate);
			if(clonedDate.getDay() === 0  ) { //is Sunday
				return true;
//			} else if(this._checkHoliday(clonedDate,start,end)) { // check for Holiday. Not working yet
//				return true;
			};
			
		},

		/*
		* Clean events to ensure correct format
		*/
		_cleanEvents : function(events) {
			var self = this;
			$.each(events, function(i, event) {
				self._cleanEvent(event);
			});
			return events;
		}
		,

		/*
		* Clean specific event
		*/
		_cleanEvent : function (event) {
			if (event.date) {
				event.start = event.date;
			}
			event.start = this._cleanDate(event.start);
			event.end = this._cleanDate(event.end);
			if (!event.end) {
				event.end = this._addDays(this._cloneDate(event.start), 1);
			}
		}
		,

		/*
		* Disable text selection of the elements in different browsers
		*/
		_disableTextSelect : function($elements) {
			$elements.each(function() {
				if ($.browser.mozilla) {//Firefox
					$(this).css('MozUserSelect', 'none');
				} else if ($.browser.msie) {//IE
					$(this).bind('selectstart', function() {
						return false;
					});
				} else {//Opera, etc.
					$(this).mousedown(function() {
						return false;
					});
				}
			});
		}
		,

		/*
		* returns the date on the first millisecond of the week
		*/
		_dateFirstDayOfWeek : function(date) {
			var self = this;
			var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
			var millisToSubtract = self._getAdjustedDayIndex(midnightCurrentDate) * 86400000;
			return new Date(midnightCurrentDate.getTime() - millisToSubtract);

		}
		,

		/*
		* returns the date on the first millisecond of the last day of the week
		*/
		_dateLastDayOfWeek : function(date) {
			var self = this;
			var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
			var millisToAdd = (6 - self._getAdjustedDayIndex(midnightCurrentDate)) * MILLIS_IN_DAY;
			return new Date(midnightCurrentDate.getTime() + millisToAdd);
		}
		,

		/*
		* gets the index of the current day adjusted based on options
		*/
		_getAdjustedDayIndex : function(date) {

			var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
			var currentDayOfStandardWeek = midnightCurrentDate.getDay();
			var days = [0,1,2,3,4,5,6];
			this._rotate(days, this.options.firstDayOfWeek);
			return days[currentDayOfStandardWeek];
		}
		,

		/*
		* returns the date on the last millisecond of the week
		*/
		_dateLastMilliOfWeek : function(date) {
			var lastDayOfWeek = this._dateLastDayOfWeek(date);
			return new Date(lastDayOfWeek.getTime() + (MILLIS_IN_DAY));

		}
		,

		/*
		* Clear the time components of a date leaving the date
		* of the first milli of day
		*/
		_clearTime : function(d) {
			d.setHours(0);
			d.setMinutes(0);
			d.setSeconds(0);
			d.setMilliseconds(0);
			return d;
		}
		,

		/*
		* add specific number of days to date
		*/
		_addDays : function(d, n, keepTime) {
			d.setDate(d.getDate() + n);
			if (keepTime) {
				return d;
			}
			return this._clearTime(d);
		}
		,

		/*
		* Rotate an array by specified number of places.
		*/
		_rotate : function(a /*array*/, p /* integer, positive integer rotate to the right, negative to the left... */) {
			for (var l = a.length, p = (Math.abs(p) >= l && (p %= l),p < 0 && (p += l),p), i, x; p; p = (Math.ceil(l / p) - 1) * p - l + (l = p)) {
				for (i = l; i > p; x = a[--i],a[i] = a[i - p],a[i - p] = x);
			}
			return a;
		}
		,

		_cloneDate : function(d) {
			return new Date(+d);
		}
		,

		/*
		* return a date for different representations
		*/
		_cleanDate : function(d) {
			if (typeof d == 'string') {
				return $.weekCalendar.parseISO8601(d, true) || Date.parse(d) || new Date(parseInt(d));
			}
			if (typeof d == 'number') {
				return new Date(d);
			}
			return d;
		}
		,

		/*
		* date formatting is adapted from
		* http://jacwright.com/projects/javascript/date_format
		*/
		_formatDate : function(date, format) {
			var options = this.options;
			var returnStr = '';
			for (var i = 0; i < format.length; i++) {
				var curChar = format.charAt(i);
				if ($.isFunction(this._replaceChars[curChar])) {
					returnStr += this._replaceChars[curChar](date, options);
				} else {
					returnStr += curChar;
				}
			}
			return returnStr;
		}
		,

		_replaceChars : {

			// Day
			d: function(date) {
				return (date.getDate() < 10 ? '0' : '') + date.getDate();
			}
			,
			D: function(date, options) {
				return options.shortDays[date.getDay()];
			}
			,
			j: function(date) {
				return date.getDate();
			}
			,
			l: function(date, options) {
				return options.longDays[date.getDay()];
			}
			,
			N: function(date) {
				return date.getDay() + 1;
			}
			,
			S: function(date) {
				return (date.getDate() % 10 == 1 && date.getDate() != 11 ? 'st' : (date.getDate() % 10 == 2 && date.getDate() != 12 ? 'nd' : (date.getDate() % 10 == 3 && date.getDate() != 13 ? 'rd' : 'th')));
			}
			,
			w: function(date) {
				return date.getDay();
			}
			,
			z: function(date) {
				return "Not Yet Supported";
			}
			,
			// Week
			W: function(date) {
				return "Not Yet Supported";
			}
			,
			// Month
			F: function(date, options) {
				return options.longMonths[date.getMonth()];
			}
			,
			m: function(date) {
				return (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1);
			}
			,
			M: function(date, options) {
				return options.shortMonths[date.getMonth()];
			}
			,
			n: function(date) {
				return date.getMonth() + 1;
			}
			,
			t: function(date) {
				return "Not Yet Supported";
			}
			,
			// Year
			L: function(date) {
				return "Not Yet Supported";
			}
			,
			o: function(date) {
				return "Not Supported";
			}
			,
			Y: function(date) {
				return date.getFullYear();
			}
			,
			y: function(date) {
				return ('' + date.getFullYear()).substr(2);
			}
			,
			// Time
			a: function(date) {
				return date.getHours() < 12 ? 'am' : 'pm';
			}
			,
			A: function(date) {
				return date.getHours() < 12 ? 'AM' : 'PM';
			}
			,
			B: function(date) {
				return "Not Yet Supported";
			}
			,
			g: function(date) {
				return date.getHours() % 12 || 12;
			}
			,
			G: function(date) {
				return date.getHours();
			}
			,
			h: function(date) {
				return ((date.getHours() % 12 || 12) < 10 ? '0' : '') + (date.getHours() % 12 || 12);
			}
			,
			H: function(date) {
				return (date.getHours() < 10 ? '0' : '') + date.getHours();
			}
			,
			i: function(date) {
				return (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
			}
			,
			s: function(date) {
				return (date.getSeconds() < 10 ? '0' : '') + date.getSeconds();
			}
			,
			// Timezone
			e: function(date) {
				return "Not Yet Supported";
			}
			,
			I: function(date) {
				return "Not Supported";
			}
			,
			O: function(date) {
				return (date.getTimezoneOffset() < 0 ? '-' : '+') + (date.getTimezoneOffset() / 60 < 10 ? '0' : '') + (date.getTimezoneOffset() / 60) + '00';
			}
			,
			T: function(date) {
				return "Not Yet Supported";
			}
			,
			Z: function(date) {
				return date.getTimezoneOffset() * 60;
			}
			,
			// Full Date/Time
			c: function(date) {
				return "Not Yet Supported";
			}
			,
			r: function(date) {
				return date.toString();
			}
			,
			U: function(date) {
				return date.getTime() / 1000;
			}
		},
		_checkHoliday : function(checkDate, start, end) {
		// This doesn't work yet. Need to find out how to check the holiday Json object

//				alert('more ' + date);
			var options = this.options;


			if (!options.holidayCheck) {
				options.holidays(start, end,
					function(h) {
						options.holidayCont = h;
//						console.log('rad 1295 ' + options.holidayCont.length);
					}
				);
				options.holidayCheck = true;
			};
//			console.error('each ' + options.holidayCont.length);

				$.each(options.holidayCont, function(i,item){
//					console.error('each ' + item);
				
				});


/*
				options.holidays(checkDate, checkDate,
					function(h) {
						$.each(h, function(i,item){
							console.error('each ' + item.date);
							});

					}
				);
*/
		}

	});

	$.extend($.ui.weekCalendar, {
		version: '1.2.2-pre',
		getter: ['getTimeslotTimes', 'getData', 'formatDate', 'formatTime'],
		defaults: {
			date: new Date(),
			timeFormat : "h:i a",
			dateFormat : "M d, Y",
			use24Hour : false,
			daysToShow : 7,
			useHolidayCSS : false,
			firstDayOfWeek : 0, // 0 = Sunday, 1 = Monday, 2 = Tuesday, ... , 6 = Saturday
			useShortDayNames: false,
			timeSeparator : " to ",
			startParam : "start",
			endParam : "end",
			businessHours : {start: 8, end: 18, limitDisplay : false},
			newEventText : "New Event",
			timeslotHeight: 20,
			useWeekNo: false, // Added by Jolle 2009-08-10
			defaultEventLength : 2,
			timeslotsPerHour : 4,
			buttons : true,
			buttonText : {
				today : "today",
				lastWeek : "&nbsp;&lt;&nbsp;",
				nextWeek : "&nbsp;&gt;&nbsp;"
			},
			scrollToHourMillis : 500,
			allowCalEventOverlap : false,
			overlapEventsSeparate: false,
			readonly: false,
			draggable : function(calEvent, element) {
				return true;
			},
			resizable : function(calEvent, element) {
				return true;
			},
			eventClick : function() {
			},
			eventRender : function(calEvent, element) {
				return element;
			},
			eventAfterRender : function(calEvent, element) {
				return element;
			},
			eventDrag : function(calEvent, element) {
			},
			eventDrop : function(calEvent, element) {
			},
			eventResize : function(calEvent, element) {
			},
			eventNew : function(calEvent, element) {
			},
			eventMouseover : function(calEvent, $event) {
			},
			eventMouseout : function(calEvent, $event) {
			},
			calendarBeforeLoad : function(calendar) {
			},
			calendarAfterLoad : function(calendar) {
			},
			noEvents : function() {
			},
			shortMonths : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
			longMonths : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
			shortDays : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
			longDays : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
		}
	});

	var MILLIS_IN_DAY = 86400000;
	var MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;

	$.weekCalendar = function() {
		return {
			parseISO8601 : function(s, ignoreTimezone) {

				// derived from http://delete.me.uk/2005/03/iso8601.html
				var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
								"(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
								"(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
				var d = s.match(new RegExp(regexp));
				if (!d) return null;
				var offset = 0;
				var date = new Date(d[1], 0, 1);
				if (d[3]) {
					date.setMonth(d[3] - 1);
				}
				if (d[5]) {
					date.setDate(d[5]);
				}
				if (d[7]) {
					date.setHours(d[7]);
				}
				if (d[8]) {
					date.setMinutes(d[8]);
				}
				if (d[10]) {
					date.setSeconds(d[10]);
				}
				if (d[12]) {
					date.setMilliseconds(Number("0." + d[12]) * 1000);
				}
				if (!ignoreTimezone) {
					if (d[14]) {
						offset = (Number(d[16]) * 60) + Number(d[17]);
						offset *= ((d[15] == '-') ? 1 : -1);
					}
					offset -= date.getTimezoneOffset();
				}
				return new Date(Number(date) + (offset * 60 * 1000));
			}
		};
	}();


})(jQuery);

/**
	* Code to return week number according to ISO 8601
	* Found here http://syn.ac/tech/19/get-the-weeknumber-with-javascript/
	* Added by Jolle 2009-08-10
 */
Date.prototype.getWeek = function() {
	var determinedate = new Date();
	determinedate.setFullYear(this.getFullYear(), this.getMonth(), this.getDate());
	var D = determinedate.getDay();
	if(D == 0) D = 7;
	determinedate.setDate(determinedate.getDate() + (4 - D));
	var YN = determinedate.getFullYear();
	var ZBDoCY = Math.floor((determinedate.getTime() - new Date(YN, 0, 1, -6)) / 86400000);
	var WN = 1 + Math.floor(ZBDoCY / 7);
	return WN;
}
